﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace Dapper.Sharding
{
    public abstract class IClient
    {
        public IClient(DataBaseType dbType, DataBaseConfig config, DataBaseVersion version = DataBaseVersion.Default)
        {
            DbType = dbType;
            Config = config;
            DbVersion = version;
        }

        #region dapper method

        public int Execute(string sql, object param = null, int? timeout = null)
        {
            using var cnn = GetConn();
            return cnn.Execute(sql, param, commandTimeout: timeout);
        }

        public object ExecuteScalar(string sql, object param = null, int? timeout = null)
        {
            using var cnn = GetConn();
            return cnn.ExecuteScalar(sql, param, commandTimeout: timeout);
        }

        public T ExecuteScalar<T>(string sql, object param = null, int? timeout = null)
        {
            using var cnn = GetConn();
            return cnn.ExecuteScalar<T>(sql, param, commandTimeout: timeout);
        }

        public dynamic QueryFirstOrDefault(string sql, object param = null, int? timeout = null)
        {
            using var cnn = GetConn();
            return cnn.QueryFirstOrDefault(sql, param, commandTimeout: timeout);
        }

        public T QueryFirstOrDefault<T>(string sql, object param = null, int? timeout = null)
        {
            using var cnn = GetConn();
            return cnn.QueryFirstOrDefault<T>(sql, param, commandTimeout: timeout);
        }

        public List<dynamic> Query(string sql, object param = null, int? timeout = null)
        {
            using var cnn = GetConn();
            return cnn.Query(sql, param, commandTimeout: timeout).AsList();
        }

        public List<T> Query<T>(string sql, object param = null, int? timeout = null)
        {
            using var cnn = GetConn();
            return cnn.Query<T>(sql, param, commandTimeout: timeout).AsList();
        }

        public void QueryMultiple(string sql, object param = null, Action<SqlMapper.GridReader> onReader = null, int? timeout = null)
        {
            using var conn = GetConn();
            using var reader = conn.QueryMultiple(sql, param, commandTimeout: timeout);
            onReader?.Invoke(reader);
        }

        public DataTable QueryDataTable(string sql, object param = null, int? timeout = null)
        {
            using var cnn = GetConn();
            return cnn.QueryDataTable(sql, param, commandTimeout: timeout);
        }

        public DataSet QueryDataSet(string sql, object param = null, int? timeout = null)
        {
            using var cnn = GetConn();
            return cnn.QueryDataSet(sql, param, commandTimeout: timeout);
        }

        #endregion

        #region dapper method async

        public async Task<int> ExecuteAsync(string sql, object param = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            using var cnn = await GetConnAsync();
            return await cnn.ExecuteAsync(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken);
        }

        public async Task<object> ExecuteScalarAsync(string sql, object param = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            using var cnn = await GetConnAsync();
            return await cnn.ExecuteScalarAsync(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken);
        }

        public async Task<T> ExecuteScalarAsync<T>(string sql, object param = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            using var cnn = await GetConnAsync();
            return await cnn.ExecuteScalarAsync<T>(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken);
        }

        public async Task<dynamic> QueryFirstOrDefaultAsync(string sql, object param = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            using var cnn = await GetConnAsync();
            return await cnn.QueryFirstOrDefaultAsync(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken);
        }

        public async Task<T> QueryFirstOrDefaultAsync<T>(string sql, object param = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            using var cnn = await GetConnAsync();
            return await cnn.QueryFirstOrDefaultAsync<T>(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken);
        }

        public async Task<List<dynamic>> QueryAsync(string sql, object param = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            using var cnn = await GetConnAsync();
            return (await cnn.QueryAsync(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken)).AsList();
        }

        public async Task<List<T>> QueryAsync<T>(string sql, object param = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            using var cnn = await GetConnAsync();
            return (await cnn.QueryAsync<T>(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken)).AsList();
        }

#if NET5_0_OR_GREATER
        public async IAsyncEnumerable<T> QueryUnbufferedAsync<T>(string sql, object param = null, int? timeout = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            using var cnn = await GetConnAsync();
            var list = cnn.QueryUnbufferedAsync<T>(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken);
            await foreach (var item in list)
            {
                yield return item;
            }
        }

        public async IAsyncEnumerable<dynamic> QueryUnbufferedAsync(string sql, object param = null, int? timeout = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            using var cnn = await GetConnAsync();
            var list = cnn.QueryUnbufferedAsync(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken);
            await foreach (var item in list)
            {
                yield return item;
            }
        }
#endif

        public async Task QueryMultipleAsync(string sql, object param = null, Action<SqlMapper.GridReader> onReader = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            using var conn = await GetConnAsync();
            using var reader = await conn.QueryMultipleAsync(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken);
            onReader?.Invoke(reader);
        }

        public async Task<DataTable> QueryDataTableAsync(string sql, object param = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            using var cnn = await GetConnAsync();
            return await cnn.QueryDataTableAsync(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken);
        }

        public async Task<DataSet> QueryDataSetAsync(string sql, object param = null, int? timeout = null, CancellationToken cancellationToken = default)
        {
            using var cnn = await GetConnAsync();
            return await cnn.QueryDataSetAsync(sql, param, commandTimeout: timeout, cancellationToken: cancellationToken);
        }

        #endregion

        #region protected method

        protected readonly LockManager Locker = new LockManager();

        protected readonly LockManagerAsync LockerAsync = new LockManagerAsync();

        protected readonly ConcurrentDictionary<string, IDatabase> DataBaseCache = new ConcurrentDictionary<string, IDatabase>();

        protected readonly ConcurrentDictionary<int, IDatabase> DataBaseCache2 = new ConcurrentDictionary<int, IDatabase>();

        protected abstract IDatabase CreateIDatabase(string name);

        #endregion

        #region common method

        public DataBaseType DbType { get; }

        public DataBaseVersion DbVersion { get; }

        public DataBaseConfig Config { get; }

        public string Database { get; set; }

        public List<string> DatabaseList { get; set; }

        public string Id { get; set; }

        public string Charset { get; set; }

        public bool AutoCreateDatabase { get; set; } = true;

        public bool AutoCreateTable { get; set; } = true;

        public bool AutoCompareTableColumn { get; set; } = false;

        public bool AutoCompareTableColumnLength { get; set; } = false;

        public bool AutoCompareTableColumnDelete { get; set; } = false;

        public virtual IDatabase GetDatabase(string name, string tablespace = null, bool useGis = false, string ext = null)
        {
            var exists = DataBaseCache.TryGetValue(name, out var val);
            if (!exists)
            {
                lock (Locker.GetObject(name))
                {
                    if (!DataBaseCache.ContainsKey(name))
                    {
                        if (AutoCreateDatabase)
                        {
                            if (!ExistsDatabase(name))
                            {
                                CreateDatabase(name, tablespace, useGis, ext);
                            }
                        }
                        DataBaseCache.TryAdd(name, CreateIDatabase(name));
                    }
                }
                val = DataBaseCache[name];
            }
            return val;
        }

        public virtual IDatabase GetDatabaseExists(string name)
        {
            var exists = DataBaseCache.TryGetValue(name, out var val);
            if (!exists)
            {
                lock (Locker.GetObject(name))
                {
                    if (!DataBaseCache.ContainsKey(name))
                    {
                        DataBaseCache.TryAdd(name, CreateIDatabase(name));
                    }
                }
                val = DataBaseCache[name];
            }
            return val;
        }

        public IDatabase GetDatabaseByConnString(string connectionString)
        {
            var code = connectionString.GetHashCode();
            var exists = DataBaseCache2.TryGetValue(code, out var val);
            if (!exists)
            {
                lock (Locker.GetObject(code.ToString()))
                {
                    if (!DataBaseCache2.ContainsKey(code))
                    {
                        IDatabase db;
                        if (DbType == DataBaseType.Oracle)
                        {
                            db = CreateIDatabase(Config.UserId);
                        }
                        else
                        {
                            var name = GetDbName(connectionString);
                            db = CreateIDatabase(name);
                        }
                        db.ConnectionString = connectionString;
                        DataBaseCache2.TryAdd(code, db);
                    }
                }
                val = DataBaseCache2[code];
            }
            return val;
        }

        public virtual async ValueTask<IDatabase> GetDatabaseAsync(string name, string tablespace = null, bool useGis = false, string ext = null)
        {
            var exists = DataBaseCache.TryGetValue(name, out var val);
            if (!exists)
            {
                var locker = LockerAsync.GetObjct(name);
                await locker.WaitAsync();
                try
                {
                    if (!DataBaseCache.ContainsKey(name))
                    {
                        if (AutoCreateDatabase)
                        {
                            if (!await ExistsDatabaseAsync(name))
                            {
                                await CreateDatabaseAsync(name, tablespace, useGis, ext);
                            }
                        }
                        DataBaseCache.TryAdd(name, CreateIDatabase(name));
                    }
                }
                finally
                {
                    locker.Release();
                }
                val = DataBaseCache[name];
            }
            return val;
        }

        public void ClearCache(string dbname = null)
        {
            if (!string.IsNullOrEmpty(dbname))
            {
                DataBaseCache.TryRemove(dbname, out _);
            }
            else
            {
                DataBaseCache.Clear();
            }
        }

        #endregion

        #region abstract method

        public abstract string ConnectionString { get; set; }

        public abstract DbConnection GetConn();

        public abstract Task<DbConnection> GetConnAsync();

        public abstract void CreateDatabase(string name, string tablespace = null, bool useGis = false, string ext = null);

        public abstract Task CreateDatabaseAsync(string name, string tablespace = null, bool useGis = false, string ext = null);

        public abstract void DropDatabase(string name);

        public abstract List<string> ShowDatabases();

        public abstract List<string> ShowDatabasesExcludeSystem();

        public abstract bool ExistsDatabase(string name);

        public abstract Task<bool> ExistsDatabaseAsync(string name);

        public abstract void Vacuum(string dbname);

        public abstract string GetDatabaseScript(string name, string tablespace = null, bool useGis = false, string ext = null);

        public abstract void CreateTablespace(string name, string dir);

        public abstract Task CreateTablespaceAsync(string name, string dir);

        public abstract void DropTablespace(string name);

        public abstract Task DropTablespaceAsync(string name);

        public abstract bool ExistsTablespace(string name);

        public abstract Task<bool> ExistsTablespaceAsync(string name);

        public abstract void SetTablespace(string database, string tablespace);

        public abstract Task SetTablespaceAsync(string database, string tablespace);

        public abstract void CloseSession(string database = null, bool closePostgres = false);

        public abstract Task CloseSessionAsync(string database = null, bool closePostgres = false);

        protected abstract string GetDbName(string connStr);

        #endregion

    }
}
